﻿using System;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class ScrollRectEx : UIBehaviour, IInitializePotentialDragHandler, IBeginDragHandler, IEndDragHandler, IDragHandler
{
    [SerializeField]
    private RectTransform m_defaultItem;
    public RectTransform defaultItem { get { return m_defaultItem; } set { m_defaultItem = value; } }

    /// <summary>
    /// 当前是否移动超出了边界
    /// </summary>
    /// <remarks>vector2 为超出边界的距离，如果返回false，则无视边界限制，content可根据惯性一直滚动</remarks>
    public Func<Vector2, bool> IsBounds = (delta) => true;

    /// <summary>
    /// content移动回调
    /// </summary>
    public event Action<Vector2> OnValueChange = (delta) => { };

    /// <summary>
    /// 视图大小改变事件
    /// </summary>
    public event Action OnViewBoundsChange = () => { };

    /// <summary>
    /// 拖拽到最底部，释放时调用
    /// </summary>
    public Action OnDragBottomMost;

    /// <summary>
    /// 拖拽到最顶部，释放时调用
    /// </summary>
    public Action OnDragTopMost;

    /// <summary>
    /// 触发拖拽到最边界事件的最小距离
    /// </summary>
    private float minDragMostDistance = 10f;

    private IScrollList scrollList;

#if UNITY_EDITOR
    protected override void OnValidate()
    {
        if (content != null || defaultItem != null)
            return;

        if (transform.childCount > 0)
        {
            var child = transform.GetChild(0);
            if (child.childCount > 0)
            {
                content = child as RectTransform;
                defaultItem = child.GetChild(0) as RectTransform;
            }
            else
            {
                content = transform as RectTransform;
                defaultItem = child as RectTransform;
                horizontal = false;
                vertical = false;
            }
        }
    }
#endif

    public ScrollList<T> Init<T>(ItemHandler<T> refreshItemHandler, ScrollListInitData<T> initData = null)
    {
        if (scrollList != null)
            return scrollList as ScrollList<T>;

        if (!m_Content)
            return null;

        Canvas.ForceUpdateCanvases();
        m_defaultItem.gameObject.SetActive(false);
        var listCom = new ScrollList<T>();
        IsBounds = listCom.IsBounds;
        OnValueChange += listCom.OnValueChange;
        OnViewBoundsChange += listCom.OnViewBoundsChange;
        enabled = IsInteractable();

        if (m_Content != viewRect)
            m_Content.pivot = Vector2.up;

        listCom.Init(this, refreshItemHandler, initData);
        scrollList = listCom;
        return listCom;
    }

    public void ZeroContent()
    {
        if (m_Content == null)
            return;

        // 不需要滚动时，不用重置位置
        if (m_Content == viewRect)
            return;

        var anchorMinPos = viewRect.rect.min +
            Vector2.Scale(m_Content.anchorMin, viewRect.rect.size);

        var anchorMaxPos = viewRect.rect.max -
            Vector2.Scale(Vector2.one - m_Content.anchorMax, viewRect.rect.size);

        var localPosition2D = new Vector2(m_Content.localPosition.x, m_Content.localPosition.y);
        var rectMinPos = m_Content.rect.min + localPosition2D;
        var rectMaxPos = m_Content.rect.max + localPosition2D;

        // 左边归零
        var leftOffset = rectMinPos.x - viewRect.rect.min.x;
        rectMinPos.x -= leftOffset;
        rectMaxPos.x -= leftOffset;

        // 上边归零
        var topOffset = viewRect.rect.max.y - rectMaxPos.y;
        rectMinPos.y += topOffset;
        rectMaxPos.y += topOffset;

        var offsetMin = rectMinPos - anchorMinPos;
        var offsetMax = rectMaxPos - anchorMaxPos;

        var sizeDelta = offsetMax - offsetMin;

        var anchoredPosition = offsetMin + Vector2.Scale(sizeDelta, m_Content.pivot);

        TrySetContentPosition(anchoredPosition);
    }

    /// <summary>
    /// 停止滚动
    /// </summary>
    public void StopMove()
    {
        m_Velocity = Vector2.zero;
    }

    /// <summary>
    /// 速度是否恒定
    /// </summary>
    private bool isConstVelocity;

    public void SetIsConstVelocity(bool isConst)
    {
        isConstVelocity = isConst;
    }

    public void SetVelocity(Vector2 velocity)
    {
        if (!horizontal)
            velocity.x = 0;
        if (!vertical)
            velocity.y = 0;

        m_Velocity = velocity;
    }

    public virtual bool IsInteractable()
    {
        if (m_Content == viewRect)
            return false;

        return m_Horizontal || m_Vertical;
    }

    #region scroll rect data

    [SerializeField]
    private RectTransform m_Content;
    public RectTransform content { get { return m_Content; } set { m_Content = value; } }

    private Vector2 lastDragPosition = Vector2.zero;

    private bool m_Dragging;

    private Bounds m_ContentBounds;
    public Bounds contentBounds { get { return m_ContentBounds; } }

    private Bounds m_ViewBounds;
    public Bounds viewBounds { get { return m_ViewBounds; } }

    private Vector2 m_Velocity;
    public Vector2 Velocity { get { return m_Velocity; } }

    [SerializeField]
    private float m_Elasticity = 0.1f;
    [SerializeField]
    private bool m_Inertia = true;
    [SerializeField]
    private float m_DecelerationRate = 0.135f;

    // 允许脱离边界的距离
    private float maxBoundsRate = 0.55f;

    // 视图
    private RectTransform m_ViewRect;
    public RectTransform viewRect
    {
        get
        {
            if (m_ViewRect == null)
                m_ViewRect = (RectTransform)transform;
            return m_ViewRect;
        }
    }

    [SerializeField]
    private bool m_Horizontal = true;
    public bool horizontal
    {
        get { return m_Horizontal; }
        set
        {
            m_Horizontal = value;
            enabled = IsInteractable();
        }
    }

    [SerializeField]
    private bool m_Vertical = true;
    public bool vertical
    {
        get { return m_Vertical; }
        set
        {
            m_Vertical = value;
            enabled = IsInteractable();
        }
    }

    private readonly Vector3[] m_Corners = new Vector3[4];

    private Bounds preViewBounds = new Bounds();
    private Vector2 perMousePos = Vector2.zero;
    private Vector2 pointerDelta = Vector2.zero;
    #endregion

    #region scroll rect event

    private Vector2 MousePosition
    {
        get
        {
            return InputTool.MousePosition;
        }
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        if (!m_Content)
            return;

        if (eventData.button != PointerEventData.InputButton.Left)
            return;

        if (!IsActive())
            return;

        UpdateBounds();

        RectTransformUtility.ScreenPointToLocalPointInRectangle(viewRect, eventData.position, eventData.pressEventCamera, out lastDragPosition);
        m_Dragging = true;
    }

    public void OnDrag(PointerEventData eventData)
    {
        if (!m_Content)
            return;

        if (!m_Dragging)
            return;

        if (eventData.button != PointerEventData.InputButton.Left)
            return;

        if (!IsActive())
            return;

        Vector2 localCursor;
        if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(viewRect, eventData.position, eventData.pressEventCamera, out localCursor))
            return;

        UpdateBounds();

        pointerDelta = localCursor - lastDragPosition;
        CheckBoundsDelta(ref pointerDelta);
        lastDragPosition = localCursor;

        Vector2 position = m_Content.anchoredPosition + pointerDelta;

        SetContentAnchoredPosition(position);
    }

    private void CheckBoundsDelta(ref Vector2 delta)
    {
        var offset = CalculateOffset(delta);

        // 令偏差趋向于maxBoundsRate时，移动的距离逐渐变少，有效移动比例由1到0
        // 公式由 1 = a * 0 + b 和 0 = a * maxBoundsRate + b 解得。

        var xRate = 1 - (1 / maxBoundsRate) * (Mathf.Abs(offset.x) / viewBounds.size.x);
        xRate = Mathf.Clamp(xRate, 0, 1);

        var yRate = 1 - (1 / maxBoundsRate) * (Mathf.Abs(offset.y) / viewBounds.size.y);
        yRate = Mathf.Clamp(yRate, 0, 1);

        delta.x *= xRate;
        delta.y *= yRate;
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        if (!m_Content)
            return;

        if (eventData.button != PointerEventData.InputButton.Left)
            return;

        CheckTopMostEvent();
        CheckBottomMostEvent();

        m_Dragging = false;
    }

    private void CheckTopMostEvent()
    {
        if (OnDragTopMost == null)
            return;

        if (viewBounds.max.y - contentBounds.max.y > minDragMostDistance)
            OnDragTopMost();
    }

    private void CheckBottomMostEvent()
    {
        if (OnDragBottomMost == null)
            return;

        if (viewBounds.min.y - contentBounds.min.y < -minDragMostDistance)
            OnDragBottomMost();
    }

    public void OnInitializePotentialDrag(PointerEventData eventData)
    {
        if (!m_Content)
            return;

        if (eventData.button != PointerEventData.InputButton.Left)
            return;

        m_Velocity = Vector2.zero;
    }

    private void LateUpdate()
    {
        if (!m_Content)
            return;

        if (scrollList != null)
        {
            scrollList.Update(out var isSkipUpdate);
            if (isSkipUpdate)
                return;
        }

        UpdateBounds();
        float deltaTime = Time.unscaledDeltaTime;
        Vector2 offset = CalculateOffset(Vector2.zero);
        if (!m_Dragging && (offset != Vector2.zero || m_Velocity != Vector2.zero))
        {
            Vector2 position = m_Content.anchoredPosition;
            var tempVelocity = m_Velocity;

            for (int axis = 0; axis < 2; axis++)
            {
                // Apply spring physics if movement is elastic and content has an offset from the view.
                if (offset[axis] != 0)
                {
                    float speed = m_Velocity[axis];
                    float smoothTime = m_Elasticity;
                    position[axis] = Mathf.SmoothDamp(m_Content.anchoredPosition[axis], m_Content.anchoredPosition[axis] + offset[axis], ref speed, smoothTime, Mathf.Infinity, deltaTime);
                    if (Mathf.Abs(speed) < 1)
                        speed = 0;
                    m_Velocity[axis] = speed;
                }
                // Else move content according to velocity with deceleration applied.
                else if (m_Inertia)
                {
                    m_Velocity[axis] *= Mathf.Pow(m_DecelerationRate, deltaTime);
                    if (Mathf.Abs(m_Velocity[axis]) < 1)
                        m_Velocity[axis] = 0;
                    position[axis] += m_Velocity[axis] * deltaTime;
                }
                // If we have neither elaticity or friction, there shouldn't be any velocity.
                else
                {
                    m_Velocity[axis] = 0;
                }
            }

            if (isConstVelocity)
                m_Velocity = tempVelocity;

            SetContentAnchoredPosition(position);
        }

        if (m_Dragging && m_Inertia)
        {
            // 悬停拖拽时，将不再拥有速度
            if (perMousePos == MousePosition)
                pointerDelta = Vector2.zero;

            perMousePos = MousePosition;

            Vector3 newVelocity = pointerDelta / deltaTime;
            m_Velocity = Vector3.Lerp(m_Velocity, newVelocity, deltaTime * 10);
        }
    }

    #endregion

    #region scroll rect internal method

    private Vector2 CalculateOffset(Vector2 delta)
    {
        return InternalCalculateOffset(ref m_ViewBounds, ref m_ContentBounds, m_Horizontal, m_Vertical, ref delta);
    }

    public virtual Vector2 InternalCalculateOffset(ref Bounds viewBounds, ref Bounds contentBounds, bool horizontal, bool vertical, ref Vector2 delta)
    {
        Vector2 offset = Vector2.zero;

        Vector2 min = contentBounds.min;
        Vector2 max = contentBounds.max;

        var offsetRange = 0.1f;

        if (horizontal)
        {
            min.x += delta.x;
            max.x += delta.x;

            float maxOffset = viewBounds.max.x - max.x;
            float minOffset = viewBounds.min.x - min.x;

            if (minOffset < -offsetRange)
                offset.x = minOffset;
            else if (maxOffset > offsetRange)
                offset.x = maxOffset;
        }

        if (vertical)
        {
            min.y += delta.y;
            max.y += delta.y;

            float maxOffset = viewBounds.max.y - max.y;
            float minOffset = viewBounds.min.y - min.y;

            if (maxOffset > offsetRange)
                offset.y = maxOffset;
            else if (minOffset < -offsetRange)
                offset.y = minOffset;
        }

        if (!IsBounds(offset))
            return Vector2.zero;

        return offset;
    }

    public void UpdateContent()
    {
        if (content == null)
            return;

        LayoutRebuilder.ForceRebuildLayoutImmediate(m_Content);
        UpdateBounds();
    }

    public void UpdateBounds()
    {
        if (m_Content == null)
            return;

        m_ViewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);
        m_ContentBounds = GetBounds(m_Content);

        Vector3 contentSize = m_ContentBounds.size;
        Vector3 contentPos = m_ContentBounds.center;
        var contentPivot = m_Content.pivot;
        AdjustBounds(ref m_ViewBounds, ref contentPivot, ref contentSize, ref contentPos);
        m_ContentBounds.size = contentSize;
        m_ContentBounds.center = contentPos;

        if (m_ViewBounds != preViewBounds && IsInteractable())
        {
            preViewBounds = m_ViewBounds;
            OnViewBoundsChange();
        }
    }

    void AdjustBounds(ref Bounds viewBounds, ref Vector2 contentPivot, ref Vector3 contentSize, ref Vector3 contentPos)
    {
        Vector3 excess = viewBounds.size - contentSize;
        if (excess.x > 0)
        {
            contentPos.x -= excess.x * (contentPivot.x - 0.5f);
            contentSize.x = viewBounds.size.x;
        }
        if (excess.y > 0)
        {
            contentPos.y -= excess.y * (contentPivot.y - 0.5f);
            contentSize.y = viewBounds.size.y;
        }
    }

    public Bounds GetBounds(RectTransform rect)
    {
        if (rect == null)
            return new Bounds();
        rect.GetWorldCorners(m_Corners);
        var viewWorldToLocalMatrix = viewRect.worldToLocalMatrix;
        return InternalGetBounds(m_Corners, ref viewWorldToLocalMatrix);
    }

    private Bounds InternalGetBounds(Vector3[] corners, ref Matrix4x4 viewWorldToLocalMatrix)
    {
        var vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
        var vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);

        for (int j = 0; j < 4; j++)
        {
            Vector3 v = viewWorldToLocalMatrix.MultiplyPoint3x4(corners[j]);
            vMin = Vector3.Min(v, vMin);
            vMax = Vector3.Max(v, vMax);
        }

        var bounds = new Bounds(vMin, Vector3.zero);
        bounds.Encapsulate(vMax);
        return bounds;
    }

    /// <summary>
    /// Sets the anchored position of the content.
    /// </summary>
    protected virtual void SetContentAnchoredPosition(Vector2 position)
    {
        if (!m_Horizontal)
            position.x = m_Content.anchoredPosition.x;
        if (!m_Vertical)
            position.y = m_Content.anchoredPosition.y;

        if (position != m_Content.anchoredPosition)
        {
            var delta = position - m_Content.anchoredPosition;
            TrySetContentPosition(position);
            UpdateBounds();
            OnValueChange(delta);
        }
    }

    public virtual void MoveContent(Vector2 delta)
    {
        if (!m_Horizontal)
            delta.x = 0;
        if (!m_Vertical)
            delta.y = 0;

        TrySetContentPosition(m_Content.anchoredPosition + delta);
        UpdateBounds();
        OnValueChange(delta);
    }

    public virtual void SetContentAnchoredPositionSimple(Vector2 position)
    {
        if (!m_Content)
            return;

        TrySetContentPosition(position);
    }

    private void TrySetContentPosition(Vector2 position)
    {
        // 不需要滚动时，不用重置位置
        if (m_Content == viewRect)
            return;

        m_Content.anchoredPosition = position;
    }

    #endregion
}